﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;

namespace Fadd.JSON
{
	/// <summary>
	/// Serializes an object into JSON.
	/// </summary>
	public class Serializer
	{
		private List<object> _referencePath = new List<object>();

		/// <summary>
		/// Serialize object.
		/// </summary>
		/// <param name="stream">Stream to write to</param>
		/// <param name="value">object to serialize.</param>
		public void Serialize(Stream stream, object value)
		{
			var writer = new TabbedStreamWriter(stream);
			WriteObject(writer, value);
			writer.Flush();
		}

		/// <summary>
		/// Serializes value into the writer.
		/// </summary>
		/// <param name="writer">Writer getting serialized object.</param>
		/// <param name="value">Object to serialize.</param>
		public void Serialize(TextWriter writer, object value)
		{
			var myWriter = new TabbedStreamWriter(writer);
			WriteObject(myWriter, value);
			writer.Flush();
		}

		/// <summary>
		/// Can't use _referencePath.Contains since it's using Equal.
		/// </summary>
		/// <param name="value"></param>
		/// <returns></returns>
		private bool ContainsReference(object value)
		{
			foreach (var o in _referencePath)
			{
				if (o == value)
					return true;
			}
			return false;
		}

		private void WriteObject(TabbedStreamWriter writer, object obj)
		{
			if (obj == null)
				throw new ArgumentNullException("obj");

			_referencePath.Add(obj);

			Type type = obj.GetType();
			writer.WriteLine("{");
			writer.Increase();

			PropertyInfo[] infos = type.GetProperties();
			PropertyInfo info;
			bool ignored = false;
			for (int i = 0; i < infos.Length; ++i)
			{
				info = infos[i];
				if (info.IsSpecialName || info.GetIndexParameters().Length > 0)
				{
					//ignored = true;
					continue;
				}

				object theValue = info.GetValue(obj, null);
				if (theValue == null)
				{
					//ignored = true;
					continue;
				}

				if (ContainsReference(theValue))
				{
					//ignored = true;
					continue;
				}

				if (i > 0 && !ignored)
					writer.WriteLine(",");

				ignored = false;
				writer.Write("\"" + info.Name + "\": ");
				WriteValue(writer, theValue.GetType(), theValue);

				// so we can identify object/interface types.
				if (info.PropertyType != typeof (object) && !info.PropertyType.IsInterface) 
					continue;

				writer.WriteLine(",");
				writer.Write("\"__" + info.Name + "\": ");
				WriteValue(writer, typeof(string), theValue.GetType().FullName + ", " + theValue.GetType().Assembly);
			}
			writer.WriteLine();

			writer.Decrease();
			writer.Write("}");
		}

		private bool WriteValue(TabbedStreamWriter writer, Type type, object obj)
		{
			if (type == typeof (string) || type == typeof (Type))
			{
				writer.Write("\"");
				writer.Write(obj);
				writer.Write("\"");
				return false;
			}
			if (type.IsEnum)
			{
				writer.Write("\"");
				writer.Write(obj.ToString());
				writer.Write("\"");
				return false;
			}
			if (type == typeof (bool))
			{
				writer.Write(obj.ToString().ToLower());
				return false;
			}
			if (type == typeof (DateTime))
			{
				writer.Write("\"");
				DateTime utc = ((DateTime) obj).ToUniversalTime();
				writer.Write(utc.ToString("o"));
				writer.Write("\"");
				return false;
			}
			if (type.IsPrimitive)
			{
				writer.Write(obj);
				return false;
			}

			if (type.IsArray)
				WriteCollection(writer, obj);
			else if (typeof (IEnumerable).IsAssignableFrom(type))
				WriteCollection(writer, obj);
			else
				WriteObject(writer, obj);
			return true;
		}

		private void WriteCollection(TabbedStreamWriter writer, object obj)
		{
			writer.WriteLine("[");
			writer.Increase();
			IEnumerator enumerator = ((IEnumerable) obj).GetEnumerator();
			if (enumerator.MoveNext())
			{
				if (WriteValue(writer, enumerator.Current.GetType(), enumerator.Current))
					writer.WriteLine();
			}

			while (enumerator.MoveNext())
			{
				writer.Write(",");
				if (WriteValue(writer, enumerator.Current.GetType(), enumerator.Current))
					writer.WriteLine();
			}
			writer.WriteLine();
			writer.Decrease();
			writer.Write("]");
		}
	}
}